Reference book: https://www.tidytextmining.com/
Sentiment analysis can be thought of as the exercise of taking a sentence, paragraph, document, or any piece of natural language, and determining whether that text’s emotional tone is positive, negative or neutral. One way to analyze the sentiment of a text is to consider the text as a combination of its individual words and the sentiment content of the whole text as the sum of the sentiment content of the individual words.
When human readers approach a text, we use our understanding of the emotional intent of words to infer whether a section of text is positive or negative, or perhaps characterized by some other more nuanced emotion like surprise or disgust.

library(tidytext)
library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
method from
print.tbl_lazy
print.tbl_sql
── Attaching packages ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.1 ──
✓ ggplot2 3.3.5 ✓ purrr 0.3.4
✓ tibble 3.1.5 ✓ dplyr 1.0.7
✓ tidyr 1.1.4 ✓ stringr 1.4.0
✓ readr 2.0.2 ✓ forcats 0.5.1
── Conflicts ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter() masks stats::filter()
x dplyr::lag() masks stats::lag()
library(textdata)
The tidytext package contains sentiments dataset. The three general-purpose lexicons are:
The above different sentiment lexicons, based on unigrams (i.e. single words) are derived from a single English word and are assigned different scores of positive/negative sentiments. Each lexicon scores a different way from the others.
AFINN scores a word with a number, which may range from -5 to +5.
bing scores a word as either positive or negative.
nrc categorizes a word under sentiment type categories such as positive, negative, anger, anticipation, disgust, fear, joy, sadness, surprise, and trust.
All of this information is tabulated in the sentiments dataset, and tidytext provides a function get_sentiments() to get specific sentiment lexicons without the columns that are not used in that lexicon.
# sample of sentiments
sample_n( get_sentiments("afinn") , 5)
# sample of sentiments
sample_n( get_sentiments("nrc") , 5)
# sample of sentiments
sample_n( get_sentiments("bing"), 5)
There are also some domain-specific sentiment lexicons available, constructed to be used with text from a specific content area.
Not every English word is in the lexicons because many English words are pretty neutral. It is important to keep in mind that these methods do not take into account qualifiers before a word, such as in “no good” or “not true”; a lexicon-based method like this is based on unigrams only.
One last caveat is that the size of the chunk of text that we use to add up unigram sentiment scores can have an effect on an analysis. A text the size of many paragraphs can often have positive and negative sentiment averaged out to about zero, while sentence-sized or paragraph-sized text often works better.
Songs Lyrics Example
Read data
# data from https://raw.githubusercontent.com/reisanar/datasets/master/BB_top100_2015.csv
lyrics_2015 <- read_csv("BB_top100_2015.csv")
Rows: 100 Columns: 6
── Column specification ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
chr (3): Song, Artist, Lyrics
dbl (3): Rank, Year, Source
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
lyrics_2015 <- select(lyrics_2015, -Year, -Source)
Take a look at the data you loaded:
head(lyrics_2015)
Let us focus on artists that have more than 1 song in the Top 100
# print artist that appear more than once in the list
lyrics_2015[duplicated(lyrics_2015$Artist),]
Goal: study the sentiment of the top 10 songs lyrics
lyrics_2015 %>%
filter(Rank %in% 1:10)
First, let us convert the text to the tidy format using unnest_tokens(). Notice that we choose the name word for the output column from unnest_tokens(). This is a convenient choice because the sentiment lexicons and stop word datasets have columns named word; performing inner joins and anti-joins is thus easier.
lyrics_2015 %>%
filter(Rank %in% 1:10) %>%
unnest_tokens(word, Lyrics)
Notice that we did not remove any stop-words!
lyrics_2015 %>%
filter(Rank %in% 1:10) %>%
unnest_tokens(word, Lyrics, token = "words") %>% filter(!word %in% stop_words$word, str_detect(word, "[a-z]"))
We could explore this dataset by first, checking the frequency of words in the top 10 songs lyrics.
First let us store our tidy text data set in a new object called bb_top_10
# BB top 10 songs lyics in 2015 (no stop-words)
bb_top_10 <- lyrics_2015 %>%
filter(Rank %in% 1:10) %>% unnest_tokens(word, Lyrics, token = "words") %>%
filter(!word %in% stop_words$word, str_detect(word, "[a-z]"))
Check the most used words:
bb_top_10 %>%
group_by(word) %>%
summarise(uses = n()) %>%
arrange(desc(uses)) %>%
head(10)
With the information above, we can graphically show a sample of frequent words
bb_top_10 %>%
group_by(word) %>%
summarise(uses = n()) %>%
arrange(desc(uses)) %>%
slice(1:15) %>%
ggplot() +
geom_bar(aes(x = word, y = uses), stat = "identity") +
coord_flip() +
theme_minimal()

Sentiment Analysis
Much as removing stop words is an anti-join operation, performing sentiment analysis is an inner join operation.
bb_top_10 %>%
inner_join(get_sentiments("bing")) %>%
count(Song, sentiment) %>%
spread(sentiment, n, fill = 0) %>%
mutate(sentiment = positive - negative)
Joining, by = "word"
And a visualization of it can tell us about the “sentiment” of each song as a whole:
bb_top_10 %>%
inner_join(get_sentiments("bing")) %>%
count(Song, sentiment) %>%
spread(sentiment, n, fill = 0) %>%
mutate(sentiment = positive - negative) %>%
ggplot() +
geom_bar(aes(x = reorder(Song, sentiment), y = sentiment), stat = "identity") +
coord_flip() +
labs(x = "", title = "Sentiment Analysis of Songs", subtitle = "Top 10 Billboard songs in 2015") +
theme_minimal()
Joining, by = "word"

What if we want to find out how the sentiment of the song varies as the song progresses? Let us use now the afinn lexicon
bb_top_10 %>%
inner_join(get_sentiments("afinn"))
Do you want to download:
Name: AFINN-111
URL: http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6010
License: Open Database License (ODbL) v1.0
Size: 78 KB (cleaned 59 KB)
Download mechanism: https
1: Yes
2: No
1
trying URL 'http://www2.imm.dtu.dk/pubdb/views/edoc_download.php/6010/zip/imm6010.zip'
Content type 'application/zip' length 16227 bytes (15 KB)
==================================================
downloaded 15 KB
Joining, by = "word"
# add an index column to keep track of the words used
bb_top_10 %>%
group_by(Song) %>%
mutate(index = row_number() ) %>%
inner_join(get_sentiments("afinn"))
Joining, by = "word"
Notice that several songs in this top 10 list, contain many words that are not part of the lexicon, and therefore were not scored:
bb_top_10 %>%
group_by(Song) %>%
mutate(index = row_number() ) %>%
inner_join(get_sentiments("afinn")) %>%
ggplot() +
geom_col(aes(x = index, y = value)) +
facet_wrap(~Song, scales = "free", ncol = 2) +
labs(title = "Sentiment Analysis of Songs using AFINN lexicon",
subtitle = "Top 10 Billboard songs in 2015",
x = "") +
theme_minimal()
Joining, by = "word"

Focus on one artist
Goal: perform sentiment analysis using the nrc lexicon for songs by Taylor Swift
lyrics_2015 %>%
filter(Artist == "taylor swift")
First, let us convert the text to the tidy format using unnest_tokens(). Notice that we choose the name word for the output column from unnest_tokens(). This is a convenient choice because the sentiment lexicons and stop word datasets have columns named word; performing inner joins and anti-joins is thus easier.
lyrics_2015 %>%
filter(Artist == "taylor swift") %>%
unnest_tokens(word, Lyrics)
Notice that we did not remove any stop-words!
We could explore this dataset by first, checking the frequency of words in the top 10 songs lyrics.
First let us store our tidy text data set in a new object
# taylor swift songs (no stop-words)
swift <- lyrics_2015 %>%
filter(Artist == "taylor swift") %>%
unnest_tokens(word, Lyrics) %>%
anti_join(stop_words)
Joining, by = "word"
Check the most used words:
swift %>%
group_by(word) %>%
summarise(uses = n()) %>%
arrange(desc(uses))
With the information above, we can graphically show a sample of frequent words
swift %>%
group_by(word) %>%
summarise(uses = n()) %>%
arrange(desc(uses)) %>%
slice(1:15) %>%
ggplot() +
geom_bar(
aes(x = reorder(word, uses), y = uses), stat = "identity") +
coord_flip() +
labs(y = "frequency of use", x = "") +
theme_minimal()

Sentiment Analysis
Using the nrc lexicon
swift %>%
inner_join(get_sentiments("nrc")) %>%
group_by(sentiment) %>%
count() %>%
ggplot() +
geom_bar(
aes(x = reorder(sentiment, n), y = n), stat = "identity") +
labs(title = "Sentiment analysis using nrc lexicon", subtitle = "Lyrics of Songs by Taylor Swift on 2015 Billboard Top 100", x = "", y = "word-count") +
theme_minimal()
Do you want to download:
Name: NRC Word-Emotion Association Lexicon
URL: http://saifmohammad.com/WebPages/lexicons.html
License: License required for commercial use. Please contact Saif M. Mohammad (saif.mohammad@nrc-cnrc.gc.ca).
Size: 22.8 MB (cleaned 424 KB)
Download mechanism: http
Citation info:
This dataset was published in Saif M. Mohammad and Peter Turney. (2013), ``Crowdsourcing a Word-Emotion Association Lexicon.'' Computational Intelligence, 29(3): 436-465.
article{mohammad13,
author = {Mohammad, Saif M. and Turney, Peter D.},
title = {Crowdsourcing a Word-Emotion Association Lexicon},
journal = {Computational Intelligence},
volume = {29},
number = {3},
pages = {436-465},
doi = {10.1111/j.1467-8640.2012.00460.x},
url = {https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1467-8640.2012.00460.x},
eprint = {https://onlinelibrary.wiley.com/doi/pdf/10.1111/j.1467-8640.2012.00460.x},
year = {2013}
}
If you use this lexicon, then please cite it.
1: Yes
2: No
1
trying URL 'http://saifmohammad.com/WebDocs/NRC-Emotion-Lexicon.zip'
Content type 'application/zip' length 24436570 bytes (23.3 MB)
==================================================
downloaded 23.3 MB
Joining, by = "word"

Show words associated to some of the sentiments:
swift_words <- swift %>%
inner_join(get_sentiments("nrc")) %>%
group_by(sentiment, word) %>%
count(mycount = n()) %>%
distinct() %>% filter(sentiment %in% c("negative", "anger",
"positive", "trust"))
Joining, by = "word"
# plot
ggplot(data = swift_words, aes(label = word)) +
ggrepel::geom_label_repel(
aes(
x = word,
y = rnorm(nrow(swift_words)),
label = word),
direction = "both",
box.padding = 0.04,
segment.color = "transparent",
size = 3) +
facet_wrap( ~ sentiment, ncol = 2) +
labs(x = "", y = "") +
theme(
axis.text.y = element_blank(),
axis.text.x = element_blank(),
axis.ticks = element_blank(),
panel.grid = element_blank(),
panel.background = element_blank())

LS0tCnRpdGxlOiAiSW50cm8gdG8gU2VudGltZW50IEFuYWx5c2lzIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpSZWZlcmVuY2UgYm9vazogCjxodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vPgoKU2VudGltZW50IGFuYWx5c2lzIGNhbiBiZSB0aG91Z2h0IG9mIGFzIHRoZSBleGVyY2lzZSBvZiB0YWtpbmcgYSBzZW50ZW5jZSwKcGFyYWdyYXBoLCBkb2N1bWVudCwgb3IgYW55IHBpZWNlIG9mIG5hdHVyYWwgbGFuZ3VhZ2UsIGFuZCBkZXRlcm1pbmluZwp3aGV0aGVyIHRoYXQgdGV4dCdzIGVtb3Rpb25hbCB0b25lIGlzIHBvc2l0aXZlLCBuZWdhdGl2ZSBvciBuZXV0cmFsLiBPbmUgd2F5IHRvCmFuYWx5emUgdGhlIHNlbnRpbWVudCBvZiBhIHRleHQgaXMgdG8gY29uc2lkZXIgdGhlIHRleHQgYXMgYSBjb21iaW5hdGlvbiBvZiBpdHMKaW5kaXZpZHVhbCB3b3JkcyBhbmQgdGhlIHNlbnRpbWVudCBjb250ZW50IG9mIHRoZSB3aG9sZSB0ZXh0IGFzIHRoZSBzdW0gb2YgdGhlCnNlbnRpbWVudCBjb250ZW50IG9mIHRoZSBpbmRpdmlkdWFsIHdvcmRzLgoKV2hlbiBodW1hbiByZWFkZXJzIGFwcHJvYWNoIGEgdGV4dCwgd2UgdXNlIG91ciB1bmRlcnN0YW5kaW5nIG9mIHRoZSBfZW1vdGlvbmFsIGludGVudF8gb2Ygd29yZHMgdG8gaW5mZXIgd2hldGhlciBhIHNlY3Rpb24gb2YgdGV4dCBpcyBwb3NpdGl2ZSBvciBuZWdhdGl2ZSwgb3IgcGVyaGFwcyBjaGFyYWN0ZXJpemVkIGJ5IHNvbWUgb3RoZXIgbW9yZSBudWFuY2VkIGVtb3Rpb24gbGlrZSBzdXJwcmlzZSBvciBkaXNndXN0LiAKCgpgYGB7ciwgZWNobyA9IEZBTFNFLCBvdXQud2lkdGg9IjIwJSJ9CmtuaXRyOjppbmNsdWRlX2dyYXBoaWNzKCJodHRwczovL2dpdGh1Yi5jb20vcmVpc2FuYXIvZmlncy9yYXcvbWFzdGVyL3RpZHlfdGV4dF9taW5pbmcucG5nIikKYGBgCgoKCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeSh0aWR5dGV4dCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkodGV4dGRhdGEpCmBgYAoKVGhlIGB0aWR5dGV4dGAgcGFja2FnZSBjb250YWlucyBgc2VudGltZW50c2AgZGF0YXNldC4gVGhlIHRocmVlIGdlbmVyYWwtcHVycG9zZSBsZXhpY29ucyBhcmU6CgotIGBBRklOTmAgZnJvbSBbRmlubiBBcnVwIE5pZWxzZW5dKGh0dHA6Ly93d3cyLmltbS5kdHUuZGsvcHViZGIvdmlld3MvcHVibGljYXRpb25fZGV0YWlscy5waHA/aWQ9NjAxMCksCgotIGBiaW5nYCBmcm9tIFtCaW5nIExpdSBhbmQgY29sbGFib3JhdG9yc10oaHR0cHM6Ly93d3cuY3MudWljLmVkdS9+bGl1Yi9GQlMvc2VudGltZW50LWFuYWx5c2lzLmh0bWwpLCBhbmQKCi0gYG5yY2AgZnJvbSBbU2FpZiBNb2hhbW1hZCBhbmQgUGV0ZXIgVHVybmV5XShodHRwOi8vc2FpZm1vaGFtbWFkLmNvbS9XZWJQYWdlcy9OUkMtRW1vdGlvbi1MZXhpY29uLmh0bSkuCgoKVGhlIGFib3ZlIGRpZmZlcmVudCBzZW50aW1lbnQgbGV4aWNvbnMsIGJhc2VkIG9uIHVuaWdyYW1zIChpLmUuIHNpbmdsZSB3b3JkcykgYXJlIGRlcml2ZWQgZnJvbSBhIHNpbmdsZSBFbmdsaXNoIHdvcmQgYW5kIGFyZSBhc3NpZ25lZCBkaWZmZXJlbnQgc2NvcmVzIG9mIHBvc2l0aXZlL25lZ2F0aXZlIHNlbnRpbWVudHMuIEVhY2ggbGV4aWNvbiBzY29yZXMgYSBkaWZmZXJlbnQgd2F5IGZyb20gdGhlIG90aGVycy4gCgotIGBBRklOTmAgc2NvcmVzIGEgd29yZCB3aXRoIGEgbnVtYmVyLCB3aGljaCBtYXkgcmFuZ2UgZnJvbSAtNSB0byArNS4gCgotIGBiaW5nYCBzY29yZXMgYSB3b3JkIGFzIGVpdGhlciBwb3NpdGl2ZSBvciBuZWdhdGl2ZS4gCgotIGBucmNgIGNhdGVnb3JpemVzIGEgd29yZCB1bmRlciBzZW50aW1lbnQgdHlwZSBjYXRlZ29yaWVzIHN1Y2ggYXMgcG9zaXRpdmUsIG5lZ2F0aXZlLCBhbmdlciwgYW50aWNpcGF0aW9uLCBkaXNndXN0LCBmZWFyLCBqb3ksIHNhZG5lc3MsIHN1cnByaXNlLCBhbmQgdHJ1c3QuCgoKQWxsIG9mIHRoaXMgaW5mb3JtYXRpb24gaXMgdGFidWxhdGVkIGluIHRoZSBzZW50aW1lbnRzIGRhdGFzZXQsIGFuZCBgdGlkeXRleHRgIHByb3ZpZGVzIGEgZnVuY3Rpb24gYGdldF9zZW50aW1lbnRzKClgIHRvIGdldCBzcGVjaWZpYyBzZW50aW1lbnQgbGV4aWNvbnMgd2l0aG91dCB0aGUgY29sdW1ucyB0aGF0IGFyZSBub3QgdXNlZCBpbiB0aGF0IGxleGljb24uCgpgYGB7ciwgZXZhbD1GQUxTRX0KIyBzYW1wbGUgb2Ygc2VudGltZW50cwpzYW1wbGVfbiggZ2V0X3NlbnRpbWVudHMoImFmaW5uIikgLCA1KQpgYGAKCmBgYHtyLCBldmFsPUZBTFNFfQojIHNhbXBsZSBvZiBzZW50aW1lbnRzCnNhbXBsZV9uKCBnZXRfc2VudGltZW50cygibnJjIikgLCA1KQpgYGAKCmBgYHtyLCBldmFsPUZBTFNFfQojIHNhbXBsZSBvZiBzZW50aW1lbnRzCnNhbXBsZV9uKCBnZXRfc2VudGltZW50cygiYmluZyIpLCA1KQpgYGAKCgpUaGVyZSBhcmUgYWxzbyBzb21lIGRvbWFpbi1zcGVjaWZpYyBzZW50aW1lbnQgKipsZXhpY29ucyoqIGF2YWlsYWJsZSwgY29uc3RydWN0ZWQgdG8gYmUgdXNlZCB3aXRoIHRleHQgZnJvbSBhIHNwZWNpZmljIGNvbnRlbnQgYXJlYS4gCgpOb3QgZXZlcnkgRW5nbGlzaCB3b3JkIGlzIGluIHRoZSBsZXhpY29ucyBiZWNhdXNlIG1hbnkgRW5nbGlzaCB3b3JkcyBhcmUgcHJldHR5IG5ldXRyYWwuIEl0IGlzIGltcG9ydGFudCB0byBrZWVwIGluIG1pbmQgdGhhdCB0aGVzZSBtZXRob2RzIGRvIG5vdCB0YWtlIGludG8gYWNjb3VudCBxdWFsaWZpZXJzIGJlZm9yZSBhIHdvcmQsIHN1Y2ggYXMgaW4gIm5vIGdvb2QiIG9yICJub3QgdHJ1ZSI7IGEgbGV4aWNvbi1iYXNlZCBtZXRob2QgbGlrZSB0aGlzIGlzIGJhc2VkIG9uIHVuaWdyYW1zIG9ubHkuIAoKT25lIGxhc3QgY2F2ZWF0IGlzIHRoYXQgdGhlIHNpemUgb2YgdGhlIGNodW5rIG9mIHRleHQgdGhhdCB3ZSB1c2UgdG8gYWRkIHVwIHVuaWdyYW0gc2VudGltZW50IHNjb3JlcyBjYW4gaGF2ZSBhbiBlZmZlY3Qgb24gYW4gYW5hbHlzaXMuIEEgdGV4dCB0aGUgc2l6ZSBvZiBtYW55IHBhcmFncmFwaHMgY2FuIG9mdGVuIGhhdmUgcG9zaXRpdmUgYW5kIG5lZ2F0aXZlIHNlbnRpbWVudCBhdmVyYWdlZCBvdXQgdG8gYWJvdXQgemVybywgd2hpbGUgc2VudGVuY2Utc2l6ZWQgb3IgcGFyYWdyYXBoLXNpemVkIHRleHQgb2Z0ZW4gd29ya3MgYmV0dGVyLgoKCgojIFNvbmdzIEx5cmljcyBFeGFtcGxlCgoKUmVhZCBkYXRhCgpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KIyBkYXRhIGZyb20gaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3JlaXNhbmFyL2RhdGFzZXRzL21hc3Rlci9CQl90b3AxMDBfMjAxNS5jc3YKbHlyaWNzXzIwMTUgPC0gcmVhZF9jc3YoIkJCX3RvcDEwMF8yMDE1LmNzdiIpCmx5cmljc18yMDE1IDwtIHNlbGVjdChseXJpY3NfMjAxNSwgLVllYXIsIC1Tb3VyY2UpCmBgYAoKVGFrZSBhIGxvb2sgYXQgdGhlIGRhdGEgeW91IGxvYWRlZDoKCmBgYHtyfQpoZWFkKGx5cmljc18yMDE1KQpgYGAKCkxldCB1cyBmb2N1cyBvbiBhcnRpc3RzIHRoYXQgaGF2ZSBtb3JlIHRoYW4gMSBzb25nIGluIHRoZSBUb3AgMTAwIAoKYGBge3J9CiMgcHJpbnQgYXJ0aXN0IHRoYXQgYXBwZWFyIG1vcmUgdGhhbiBvbmNlIGluIHRoZSBsaXN0Cmx5cmljc18yMDE1W2R1cGxpY2F0ZWQobHlyaWNzXzIwMTUkQXJ0aXN0KSxdCmBgYAoKR29hbDogc3R1ZHkgdGhlIHNlbnRpbWVudCBvZiB0aGUgdG9wIDEwIHNvbmdzIGx5cmljcwoKCmBgYHtyfQpseXJpY3NfMjAxNSAlPiUgCiAgZmlsdGVyKFJhbmsgJWluJSAxOjEwKQpgYGAKCgpGaXJzdCwgbGV0IHVzIGNvbnZlcnQgdGhlIHRleHQgdG8gdGhlIHRpZHkgZm9ybWF0IHVzaW5nIGB1bm5lc3RfdG9rZW5zKClgLiBOb3RpY2UgdGhhdCB3ZSBjaG9vc2UgdGhlIG5hbWUgYHdvcmRgIGZvciB0aGUgb3V0cHV0IGNvbHVtbiBmcm9tIGB1bm5lc3RfdG9rZW5zKClgLiBUaGlzIGlzIGEgY29udmVuaWVudCBjaG9pY2UgYmVjYXVzZSB0aGUgc2VudGltZW50IGxleGljb25zIGFuZCBzdG9wIHdvcmQgZGF0YXNldHMgaGF2ZSBjb2x1bW5zIG5hbWVkIGB3b3JkYDsgcGVyZm9ybWluZyBpbm5lciBqb2lucyBhbmQgYW50aS1qb2lucyBpcyB0aHVzIGVhc2llci4KCgpgYGB7cn0KbHlyaWNzXzIwMTUgJT4lIAogIGZpbHRlcihSYW5rICVpbiUgMToxMCkgJT4lCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCBMeXJpY3MpCmBgYAoKTm90aWNlIHRoYXQgd2UgZGlkIG5vdCByZW1vdmUgYW55IHN0b3Atd29yZHMhCgpgYGB7cn0KbHlyaWNzXzIwMTUgJT4lCiAgZmlsdGVyKFJhbmsgJWluJSAxOjEwKSAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIEx5cmljcywgdG9rZW4gPSAid29yZHMiKSAlPiUgZmlsdGVyKCF3b3JkICVpbiUgc3RvcF93b3JkcyR3b3JkLCBzdHJfZGV0ZWN0KHdvcmQsICJbYS16XSIpKQpgYGAKCgpXZSBjb3VsZCBleHBsb3JlIHRoaXMgZGF0YXNldCBieSBmaXJzdCwgY2hlY2tpbmcgdGhlIGZyZXF1ZW5jeSBvZiB3b3JkcyBpbiB0aGUgdG9wIDEwIHNvbmdzIGx5cmljcy4KCkZpcnN0IGxldCB1cyBzdG9yZSBvdXIgdGlkeSB0ZXh0IGRhdGEgc2V0IGluIGEgbmV3IG9iamVjdCBjYWxsZWQgYGJiX3RvcF8xMGAKCmBgYHtyfQojIEJCIHRvcCAxMCBzb25ncyBseWljcyBpbiAyMDE1IChubyBzdG9wLXdvcmRzKQpiYl90b3BfMTAgPC0gbHlyaWNzXzIwMTUgJT4lIAogIGZpbHRlcihSYW5rICVpbiUgMToxMCkgJT4lIHVubmVzdF90b2tlbnMod29yZCwgTHlyaWNzLCB0b2tlbiA9ICJ3b3JkcyIpICU+JSAKICBmaWx0ZXIoIXdvcmQgJWluJSBzdG9wX3dvcmRzJHdvcmQsIHN0cl9kZXRlY3Qod29yZCwgIlthLXpdIikpCmBgYAoKQ2hlY2sgdGhlIG1vc3QgdXNlZCB3b3JkczoKCmBgYHtyfQpiYl90b3BfMTAgJT4lCiAgZ3JvdXBfYnkod29yZCkgJT4lCiAgc3VtbWFyaXNlKHVzZXMgPSBuKCkpICU+JQogIGFycmFuZ2UoZGVzYyh1c2VzKSkgJT4lCiAgaGVhZCgxMCkKYGBgCgpXaXRoIHRoZSBpbmZvcm1hdGlvbiBhYm92ZSwgd2UgY2FuIGdyYXBoaWNhbGx5IHNob3cgYSBzYW1wbGUgb2YgZnJlcXVlbnQgd29yZHMKCmBgYHtyfQogYmJfdG9wXzEwICU+JQogIGdyb3VwX2J5KHdvcmQpICU+JQogIHN1bW1hcmlzZSh1c2VzID0gbigpKSAlPiUKICBhcnJhbmdlKGRlc2ModXNlcykpICU+JQogIHNsaWNlKDE6MTUpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2JhcihhZXMoeCA9IHdvcmQsIHkgPSB1c2VzKSwgc3RhdCA9ICJpZGVudGl0eSIpICsKICBjb29yZF9mbGlwKCkgKwogIHRoZW1lX21pbmltYWwoKQpgYGAKCgoKCiMjIFNlbnRpbWVudCBBbmFseXNpcyAKCk11Y2ggYXMgcmVtb3Zpbmcgc3RvcCB3b3JkcyBpcyBhbiBhbnRpLWpvaW4gb3BlcmF0aW9uLCBwZXJmb3JtaW5nIHNlbnRpbWVudCBhbmFseXNpcyBpcyBhbiBpbm5lciBqb2luIG9wZXJhdGlvbi4KCmBgYHtyfQpiYl90b3BfMTAgJT4lCiAgaW5uZXJfam9pbihnZXRfc2VudGltZW50cygiYmluZyIpKSAlPiUKICBjb3VudChTb25nLCBzZW50aW1lbnQpICU+JQogIHNwcmVhZChzZW50aW1lbnQsIG4sIGZpbGwgPSAwKSAlPiUKICBtdXRhdGUoc2VudGltZW50ID0gcG9zaXRpdmUgLSBuZWdhdGl2ZSkKYGBgCgpBbmQgYSB2aXN1YWxpemF0aW9uIG9mIGl0IGNhbiB0ZWxsIHVzIGFib3V0IHRoZSAic2VudGltZW50IiBvZiBlYWNoIHNvbmcgYXMgYSB3aG9sZToKCmBgYHtyfQpiYl90b3BfMTAgJT4lCiAgaW5uZXJfam9pbihnZXRfc2VudGltZW50cygiYmluZyIpKSAlPiUKICBjb3VudChTb25nLCBzZW50aW1lbnQpICU+JQogIHNwcmVhZChzZW50aW1lbnQsIG4sIGZpbGwgPSAwKSAlPiUKICBtdXRhdGUoc2VudGltZW50ID0gcG9zaXRpdmUgLSBuZWdhdGl2ZSkgJT4lCiAgZ2dwbG90KCkgKwogIGdlb21fYmFyKGFlcyh4ID0gcmVvcmRlcihTb25nLCBzZW50aW1lbnQpLCB5ID0gc2VudGltZW50KSwgc3RhdCA9ICJpZGVudGl0eSIpICsKICBjb29yZF9mbGlwKCkgKwogIGxhYnMoeCA9ICIiLCB0aXRsZSA9ICJTZW50aW1lbnQgQW5hbHlzaXMgb2YgU29uZ3MiLCBzdWJ0aXRsZSA9ICJUb3AgMTAgQmlsbGJvYXJkIHNvbmdzIGluIDIwMTUiKSArCiAgdGhlbWVfbWluaW1hbCgpCmBgYAoKCldoYXQgaWYgd2Ugd2FudCB0byBmaW5kIG91dCBob3cgdGhlIHNlbnRpbWVudCBvZiB0aGUgc29uZyB2YXJpZXMgYXMgdGhlIHNvbmcgcHJvZ3Jlc3Nlcz8gTGV0IHVzIHVzZSBub3cgdGhlIGBhZmlubmAgbGV4aWNvbgoKYGBge3J9CmJiX3RvcF8xMCAlPiUKICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJhZmlubiIpKQpgYGAKCgpgYGB7cn0KIyBhZGQgYW4gaW5kZXggY29sdW1uIHRvIGtlZXAgdHJhY2sgb2YgdGhlIHdvcmRzIHVzZWQgCmJiX3RvcF8xMCAlPiUKICBncm91cF9ieShTb25nKSAlPiUKICBtdXRhdGUoaW5kZXggPSByb3dfbnVtYmVyKCkgKSAlPiUKICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJhZmlubiIpKQpgYGAKCgpOb3RpY2UgdGhhdCBzZXZlcmFsIHNvbmdzIGluIHRoaXMgdG9wIDEwIGxpc3QsIGNvbnRhaW4gbWFueSB3b3JkcyB0aGF0IGFyZSBub3QgcGFydCBvZiB0aGUgbGV4aWNvbiwgYW5kIHRoZXJlZm9yZSB3ZXJlIG5vdCBzY29yZWQ6CgpgYGB7cn0KYmJfdG9wXzEwICU+JQogIGdyb3VwX2J5KFNvbmcpICU+JQogIG11dGF0ZShpbmRleCA9IHJvd19udW1iZXIoKSApICU+JQogIGlubmVyX2pvaW4oZ2V0X3NlbnRpbWVudHMoImFmaW5uIikpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2NvbChhZXMoeCA9IGluZGV4LCB5ID0gdmFsdWUpKSArCiAgZmFjZXRfd3JhcCh+U29uZywgc2NhbGVzID0gImZyZWUiLCBuY29sID0gMikgKwogIGxhYnModGl0bGUgPSAiU2VudGltZW50IEFuYWx5c2lzIG9mIFNvbmdzIHVzaW5nIEFGSU5OIGxleGljb24iLAogICAgICAgc3VidGl0bGUgPSAiVG9wIDEwIEJpbGxib2FyZCBzb25ncyBpbiAyMDE1IiwKICAgICAgIHggPSAiIikgKwogIHRoZW1lX21pbmltYWwoKQpgYGAKCgojIEZvY3VzIG9uIG9uZSBhcnRpc3QKCgoKR29hbDogcGVyZm9ybSBzZW50aW1lbnQgYW5hbHlzaXMgdXNpbmcgdGhlIGBucmNgIGxleGljb24gZm9yIHNvbmdzIGJ5IFRheWxvciBTd2lmdAoKYGBge3J9Cmx5cmljc18yMDE1ICU+JQogIGZpbHRlcihBcnRpc3QgPT0gInRheWxvciBzd2lmdCIpCmBgYAoKCkZpcnN0LCBsZXQgdXMgY29udmVydCB0aGUgdGV4dCB0byB0aGUgdGlkeSBmb3JtYXQgdXNpbmcgYHVubmVzdF90b2tlbnMoKWAuIE5vdGljZSB0aGF0IHdlIGNob29zZSB0aGUgbmFtZSBgd29yZGAgZm9yIHRoZSBvdXRwdXQgY29sdW1uIGZyb20gYHVubmVzdF90b2tlbnMoKWAuIFRoaXMgaXMgYSBjb252ZW5pZW50IGNob2ljZSBiZWNhdXNlIHRoZSBzZW50aW1lbnQgbGV4aWNvbnMgYW5kIHN0b3Agd29yZCBkYXRhc2V0cyBoYXZlIGNvbHVtbnMgbmFtZWQgYHdvcmRgOyBwZXJmb3JtaW5nIGlubmVyIGpvaW5zIGFuZCBhbnRpLWpvaW5zIGlzIHRodXMgZWFzaWVyLgoKCmBgYHtyfQpseXJpY3NfMjAxNSAlPiUKICBmaWx0ZXIoQXJ0aXN0ID09ICJ0YXlsb3Igc3dpZnQiKSAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIEx5cmljcykKYGBgCgpOb3RpY2UgdGhhdCB3ZSBkaWQgbm90IHJlbW92ZSBhbnkgc3RvcC13b3JkcyEKCgpXZSBjb3VsZCBleHBsb3JlIHRoaXMgZGF0YXNldCBieSBmaXJzdCwgY2hlY2tpbmcgdGhlIGZyZXF1ZW5jeSBvZiB3b3JkcyBpbiB0aGUgdG9wIDEwIHNvbmdzIGx5cmljcy4KCkZpcnN0IGxldCB1cyBzdG9yZSBvdXIgdGlkeSB0ZXh0IGRhdGEgc2V0IGluIGEgbmV3IG9iamVjdAoKYGBge3J9CiMgdGF5bG9yIHN3aWZ0IHNvbmdzIChubyBzdG9wLXdvcmRzKQpzd2lmdCA8LSBseXJpY3NfMjAxNSAlPiUKICBmaWx0ZXIoQXJ0aXN0ID09ICJ0YXlsb3Igc3dpZnQiKSAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIEx5cmljcykgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMpCmBgYAoKQ2hlY2sgdGhlIG1vc3QgdXNlZCB3b3JkczoKCmBgYHtyfQpzd2lmdCAlPiUKICBncm91cF9ieSh3b3JkKSAlPiUKICBzdW1tYXJpc2UodXNlcyA9IG4oKSkgJT4lCiAgYXJyYW5nZShkZXNjKHVzZXMpKQpgYGAKCldpdGggdGhlIGluZm9ybWF0aW9uIGFib3ZlLCB3ZSBjYW4gZ3JhcGhpY2FsbHkgc2hvdyBhIHNhbXBsZSBvZiBmcmVxdWVudCB3b3JkcwoKYGBge3J9CnN3aWZ0ICU+JQogIGdyb3VwX2J5KHdvcmQpICU+JQogIHN1bW1hcmlzZSh1c2VzID0gbigpKSAlPiUKICBhcnJhbmdlKGRlc2ModXNlcykpICU+JQogIHNsaWNlKDE6MTUpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2JhcigKICAgIGFlcyh4ID0gcmVvcmRlcih3b3JkLCB1c2VzKSwgeSA9IHVzZXMpLCBzdGF0ID0gImlkZW50aXR5IikgKwogIGNvb3JkX2ZsaXAoKSArCiAgbGFicyh5ID0gImZyZXF1ZW5jeSBvZiB1c2UiLCB4ID0gIiIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgoKCgojIyBTZW50aW1lbnQgQW5hbHlzaXMgCgpVc2luZyB0aGUgYG5yY2AgbGV4aWNvbgoKYGBge3J9CnN3aWZ0ICU+JQogIGlubmVyX2pvaW4oZ2V0X3NlbnRpbWVudHMoIm5yYyIpKSAlPiUKICBncm91cF9ieShzZW50aW1lbnQpICU+JQogIGNvdW50KCkgJT4lCiAgZ2dwbG90KCkgKwogIGdlb21fYmFyKAogICAgYWVzKHggPSByZW9yZGVyKHNlbnRpbWVudCwgbiksIHkgPSBuKSwgc3RhdCA9ICJpZGVudGl0eSIpICsKICBsYWJzKHRpdGxlID0gIlNlbnRpbWVudCBhbmFseXNpcyB1c2luZyBucmMgbGV4aWNvbiIsIHN1YnRpdGxlID0gIkx5cmljcyBvZiBTb25ncyBieSBUYXlsb3IgU3dpZnQgb24gMjAxNSBCaWxsYm9hcmQgVG9wIDEwMCIsICB4ID0gIiIsIHkgPSAid29yZC1jb3VudCIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgpTaG93IHdvcmRzIGFzc29jaWF0ZWQgdG8gc29tZSBvZiB0aGUgc2VudGltZW50czoKCmBgYHtyfQpzd2lmdF93b3JkcyA8LSBzd2lmdCAlPiUKICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJucmMiKSkgJT4lCiAgZ3JvdXBfYnkoc2VudGltZW50LCB3b3JkKSAlPiUKICBjb3VudChteWNvdW50ID0gbigpKSAlPiUKICBkaXN0aW5jdCgpICU+JSBmaWx0ZXIoc2VudGltZW50ICVpbiUgYygibmVnYXRpdmUiLCAiYW5nZXIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb3NpdGl2ZSIsICJ0cnVzdCIpKQojIHBsb3QKZ2dwbG90KGRhdGEgPSBzd2lmdF93b3JkcywgYWVzKGxhYmVsID0gd29yZCkpICsKICBnZ3JlcGVsOjpnZW9tX2xhYmVsX3JlcGVsKAogICAgYWVzKAogICAgICB4ID0gd29yZCwKICAgICAgeSA9IHJub3JtKG5yb3coc3dpZnRfd29yZHMpKSwKICAgICAgbGFiZWwgPSB3b3JkKSwKICAgIGRpcmVjdGlvbiA9ICJib3RoIiwKICAgIGJveC5wYWRkaW5nID0gMC4wNCwKICAgIHNlZ21lbnQuY29sb3IgPSAidHJhbnNwYXJlbnQiLAogICAgc2l6ZSA9IDMpICsKICBmYWNldF93cmFwKCB+IHNlbnRpbWVudCwgbmNvbCA9IDIpICsKICBsYWJzKHggPSAiIiwgeSA9ICIiKSArCiAgdGhlbWUoCiAgICBheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aWNrcyA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIHBhbmVsLmdyaWQgPSBlbGVtZW50X2JsYW5rKCksCiAgICBwYW5lbC5iYWNrZ3JvdW5kID0gZWxlbWVudF9ibGFuaygpKQpgYGAK